File size: 5,365 Bytes
4a6ccb0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
114
115
"""

Does this Space read a PDF the way the corpus was read?



This is the test the whole design rests on. Part A's index and Part B's F1 of 0.945 were measured

on `skeleton_masked` and `payload_window` strings produced by the EDA notebook's extractor. If

`corpus_text.py` produces even slightly different text, those numbers stop describing this app and

become decoration.



So it pulls real PDFs out of the generation repo, runs them through `corpus_text.py`, and compares

the result **character by character** against the published parquet. Not "close enough" — identical.



The same argument applies to the family-naming model, for the same reason and with less margin: it

was fitted on seventeen numeric columns of that parquet, and `doc_features.py` recomputes them from

an uploaded file. A feature that drifts does not throw - it shifts the model's input distribution

and the model keeps answering confidently. So the numeric block is checked against the published

columns too, and the assembled feature row is checked for width against the fitted estimator.



    python test_fidelity.py [n_files]

"""

import sys

import numpy as np
import pandas as pd

import corpus_text
import doc_features

CORPUS = ("https://huggingface.co/datasets/Cyber-security-final-project/"
          "HARMLESS_Synthetic_Injected_PDFs_EDA/resolve/main/Datasets/"
          "synthetic_corpus_part2_clustered.parquet")
GENERATION_REPO = "Cyber-security-final-project/Generated_Injected_PDFs_HARMLESS"


def main(n=8):
    from huggingface_hub import hf_hub_download

    corpus = pd.read_parquet(CORPUS).set_index("file_id")

    # Injected and clean both, and not all from one family: masking and binary-stream handling
    # differ between them, and a test that only saw one would pass on a broken extractor.
    ids = list(corpus.index)
    sample = ids[::max(1, len(ids) // n)][:n]

    failures = 0
    for fid in sample:
        row = corpus.loc[fid]
        try:
            path = hf_hub_download(GENERATION_REPO, f"Output_PDFs/{fid}", repo_type="dataset")
        except Exception as e:
            print(f"  ? {fid}: could not fetch ({type(e).__name__})")
            continue

        data = open(path, "rb").read()
        skeleton, truncated, dropped = corpus_text.build_skeleton(data)
        masked = corpus_text.mask_leaks(skeleton)
        window = corpus_text.payload_window(skeleton)

        checks = {"skeleton_masked": (masked, row["skeleton_masked"]),
                  "payload_window": (window, row["payload_window"])}

        # The single-window path must also be what the triage returns first for a marked file,
        # or the model reads a different string here than the corpus was scored on.
        top = corpus_text.candidate_windows(skeleton, cover_all=False)[0]

        bad = [k for k, (got, want) in checks.items() if got != want]

        # The family model's document-shape block, against the columns it was fitted on. Compared
        # with a relative tolerance rather than exactly: the entropies and the compression ratio
        # are floats that went through a parquet round-trip, and demanding bit-equality of those
        # would fail on storage rather than on a real drift.
        stats = doc_features.describe(path, data, skeleton, truncated, dropped)
        drifted = [k for k in doc_features.NUMERIC
                   if k in row.index
                   and not np.isclose(float(stats[k]), float(row[k]), rtol=1e-4, atol=1e-6)]
        if drifted:
            bad += [f"feature {k}" for k in drifted]
            for k in drifted:
                print(f"      {k}: computed {stats[k]}, corpus has {row[k]}")

        if bad:
            failures += 1
            print(f"  x {fid}: differs in {', '.join(bad)}")
            for k in [b for b in bad if not b.startswith("feature ")]:
                got, want = checks[k]
                print(f"      {k}: got {len(got):,} chars, corpus has {len(want):,}")
        else:
            note = "head" if top["is_head"] else f"{len(top['families'])} family marker(s)"
            print(f"  . {fid}: identical  (triage top = {note}, "
                  f"{len(doc_features.NUMERIC)} features match)")

    print(f"\n{len(sample) - failures}/{len(sample)} identical to the published corpus")

    # Width of the assembled row against the fitted estimator. Cheap, and it is the check that
    # catches a retrain that added a feature block without anyone updating `build_features`.
    try:
        import family_model
        if family_model.AVAILABLE:
            row = family_model.build_features(
                skeleton, {"pred_family": "none", "pred_injected": 0, "parse_ok": 1},
                stats, np.zeros(len(family_model.CLASSES), dtype=np.float32))
            expected = family_model.load_model().n_features_in_
            ok = row.shape[0] == expected
            print(f"{'.' if ok else 'x'} feature row is {row.shape[0]} wide, "
                  f"the fitted model expects {expected}")
            failures += 0 if ok else 1
    except Exception as e:
        print(f"? family model not checked: {type(e).__name__}: {e}")

    return 1 if failures else 0


if __name__ == "__main__":
    sys.exit(main(int(sys.argv[1]) if len(sys.argv) > 1 else 8))