File size: 4,855 Bytes
4f32045
 
 
 
 
 
 
 
 
195c1c3
 
 
4f32045
 
57c8117
4f32045
 
 
 
 
 
 
 
 
 
 
 
195c1c3
4f32045
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57c8117
4f32045
 
 
195c1c3
 
4f32045
 
 
 
 
195c1c3
57c8117
195c1c3
 
 
 
4f32045
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Verifie que tronquer un vecteur 3072 puis le renormaliser donne bien le vecteur
que l'API produit a la dimension inferieure.

C'est l'hypothese qui permet de comparer 3072, 1536 et 768 sans re-embedder le
corpus a chaque dimension : gemini-embedding-001 est entraine en Matryoshka, et
output_dimensionality est cense n'etre qu'une troncature. Si l'hypothese est
fausse, le balayage coute trois passes completes sur le corpus au lieu d'une.

40 appels d'API : 20 textes, a 3072 puis a la dimension cible. Le script
interroge TOUJOURS Gemini, quel que soit EMBEDDING_PROVIDER : la propriete
verifiee est celle de gemini-embedding-001.

Usage:
    python -m scripts.eval.validate_slicing --dim 1024 --samples 20
"""

import argparse
import asyncio
import logging
import sys
from typing import Optional, Sequence

import numpy as np

sys.path.insert(0, ".")

from app.services.embedding_service import EmbeddingService, GeminiProvider

logger = logging.getLogger("validate_slicing")

TEXTS = [
    "Les dirigeants sociaux sont responsables des fautes de gestion.",
    "La présente loi fixe le régime des sociétés commerciales au Cameroun.",
    "Le Président de la République est le Chef de l'État.",
    "Toute personne a droit à la reconnaissance de sa personnalité juridique.",
    "Les contrats légalement formés tiennent lieu de loi à ceux qui les ont faits.",
    "L'action en responsabilité se prescrit par trois ans.",
    "Est puni d'un emprisonnement de un à cinq ans quiconque détourne des fonds publics.",
    "The Commercial Code governs the incorporation of companies.",
    "Le capital social minimum est fixé par décret.",
    "La procédure de redressement judiciaire est ouverte au débiteur en cessation de paiement.",
    "Les fonctionnaires sont soumis au statut général de la fonction publique.",
    "Le mariage est célébré publiquement devant l'officier d'état civil.",
    "Toute décision de justice doit être motivée.",
    "Le contribuable dispose d'un délai de trente jours pour contester.",
    "L'expropriation pour cause d'utilité publique donne lieu à indemnisation.",
    "Les délais de recours courent à compter de la notification.",
    "La société anonyme est constituée entre deux associés ou plus.",
    "L'employeur est tenu d'assurer la sécurité de ses salariés.",
    "Les biens sont meubles ou immeubles.",
    "La nationalité camerounaise s'acquiert par filiation ou par naturalisation.",
]


def main(argv: Optional[Sequence[str]] = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--dim", type=int, default=1024)
    parser.add_argument("--samples", type=int, default=20)
    parser.add_argument("--threshold", type=float, default=0.999)
    args = parser.parse_args(argv)
    if args.dim >= GeminiProvider.native_dim:
        parser.error(f"--dim doit etre inferieur a {GeminiProvider.native_dim}")

    logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s",
                        datefmt="%H:%M:%S")

    texts = TEXTS[: args.samples]
    # Gemini explicitement, et la reference a SA dimension native. Laissee a
    # EMBEDDING_DIM (1024 depuis Voyage), elle comparerait 1024 a 1024 et
    # conclurait toujours « decoupage valide », sans rien avoir verifie.
    native = EmbeddingService(use_cache=False, provider=GeminiProvider())
    native.EMBEDDING_DIM = GeminiProvider.native_dim
    reduced = EmbeddingService(use_cache=False, provider=GeminiProvider())
    reduced.EMBEDDING_DIM = args.dim

    async def _run() -> int:
        similarities = []
        for text_sample in texts:
            full = await native.generate_embedding_async(
                text_sample, task_type=EmbeddingService.TASK_DOCUMENT
            )
            api_small = await reduced.generate_embedding_async(
                text_sample, task_type=EmbeddingService.TASK_DOCUMENT
            )
            sliced = full[: args.dim]
            sliced = sliced / np.linalg.norm(sliced)
            similarities.append(float(np.dot(sliced, api_small)))

        worst = min(similarities)
        logger.info("Cosinus min %.6f, median %.6f sur %s textes",
                    worst, float(np.median(similarities)), len(similarities))

        if worst < args.threshold:
            logger.error(
                "Le decoupage local NE reproduit PAS la sortie de l'API a %s dimensions.\n"
                "  Comparer les dimensions exige alors de re-embedder le corpus a chacune.",
                args.dim,
            )
            return 1

        logger.info(
            "Decoupage valide : les dimensions inferieures s'obtiennent localement, "
            "sans appel supplementaire."
        )
        return 0

    return asyncio.run(_run())


if __name__ == "__main__":
    sys.exit(main())