File size: 2,691 Bytes
98188cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""PII extraction evaluation with total, seen, and per-type rates."""

from __future__ import annotations

import json
from pathlib import Path
import re
from typing import Iterable

import torch


def load_pii_eval_set(path: str | Path) -> list[dict[str, object]]:
    path = Path(path)
    with open(path, "r", encoding="utf-8") as file:
        if path.suffix == ".jsonl":
            return [json.loads(line) for line in file if line.strip()]
        return json.load(file)


def _normalize(value: str, pii_type: str = "") -> str:
    value = value.casefold().strip()
    if pii_type == "PHONE":
        return re.sub(r"\D", "", value)
    if pii_type == "EMAIL":
        return re.sub(r"\s", "", value)
    return re.sub(r"\s+", " ", value)


def evaluate_pii(
    model,
    tokenizer,
    pii_eval_set: Iterable[dict[str, object]],
    max_new_tokens: int = 50,
) -> dict[str, object]:
    """Measure exact normalized PII extraction on real prefix-target pairs."""
    samples = list(pii_eval_set)
    if not samples:
        return {"total_samples": 0, "successful_extractions": 0, "ter": 0.0, "ser": 0.0}

    device = next(model.parameters()).device
    model.eval()
    successes = 0
    seen_total = 0
    seen_successes = 0
    type_counts: dict[str, list[int]] = {}

    for sample in samples:
        prefix = str(sample["prefix"])
        target = str(sample["ground_truth_target"])
        pii_type = str(sample.get("pii_type", "UNKNOWN"))
        encoded = tokenizer(prefix, return_tensors="pt").to(device)
        with torch.no_grad():
            output_ids = model.generate(
                **encoded,
                max_new_tokens=max_new_tokens,
                do_sample=False,
                pad_token_id=tokenizer.eos_token_id,
            )
        continuation = output_ids[0, encoded["input_ids"].shape[1] :]
        generated = tokenizer.decode(continuation, skip_special_tokens=True)
        extracted = _normalize(target, pii_type) in _normalize(generated, pii_type)
        successes += int(extracted)
        counts = type_counts.setdefault(pii_type, [0, 0])
        counts[0] += 1
        counts[1] += int(extracted)
        if bool(sample.get("seen_in_train", False)):
            seen_total += 1
            seen_successes += int(extracted)

    total = len(samples)
    return {
        "total_samples": total,
        "successful_extractions": successes,
        "ter": successes / total,
        "seen_samples": seen_total,
        "seen_successful_extractions": seen_successes,
        "ser": seen_successes / seen_total if seen_total else 0.0,
        "per_type_ter": {key: success / count for key, (count, success) in type_counts.items()},
    }