Skip to content

Commit af8d97b

Browse files
committed
new tests and tests results
1 parent db3a5a3 commit af8d97b

37 files changed

Lines changed: 5304 additions & 254 deletions

tests/embeddings_model_tsdae.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from pathlib import Path
2+
from tqdm import tqdm
3+
4+
from tsdae import TSDAE
5+
import nltk
6+
7+
from test_2_CVE_db import iter_cve_json, extract_text
8+
9+
# --- NLTK setup (reuse your custom dir if you want) ---
10+
nltk_data_dir = Path(__file__).parent.parent / ".venv" / "nltk_data"
11+
nltk_data_dir.mkdir(parents=True, exist_ok=True)
12+
13+
nltk.data.path.insert(0, str(nltk_data_dir))
14+
try:
15+
nltk.data.find("tokenizers/punkt_tab/english")
16+
except LookupError:
17+
nltk.download("punkt_tab", download_dir=str(nltk_data_dir))
18+
19+
# --- Load CVE corpus ---
20+
dataset_root = (
21+
Path(__file__).parent.parent.parent.parent / "datasets/cvelistV5-main"
22+
)
23+
print(f"Loading dataset at {dataset_root}")
24+
25+
ids, corpus = [], []
26+
print("Start JSON iteration")
27+
for _, j in tqdm(iter_cve_json(dataset_root, 2013, 2018)):
28+
cve_id, title, text = extract_text(j)
29+
ids.append(cve_id)
30+
corpus.append(title + "\n" + text)
31+
32+
if not corpus:
33+
raise SystemExit("No CVE JSON files found.")
34+
35+
# --- TSDAE training ---
36+
model_name = "sentence-transformers/all-MiniLM-L6-v2"
37+
38+
tsdae = TSDAE(
39+
model_name=model_name,
40+
# you can tweak these hyperparameters:
41+
max_seq_length=256,
42+
corruption_rate=0.3,
43+
)
44+
45+
# tsdae expects a list of sentences; corpus is already a list[str]
46+
train_dataset = tsdae.load_dataset_from_list(corpus)
47+
48+
output_path = Path(__file__).parent.parent / "domain_adapted_model_tsdae"
49+
output_path.mkdir(exist_ok=True)
50+
51+
model = tsdae.train(
52+
train_dataset=train_dataset,
53+
output_path=str(output_path),
54+
num_epochs=1,
55+
batch_size=8,
56+
learning_rate=3e-5,
57+
)
58+
59+
print(f"TSDAE model saved to: {output_path}")

tests/embeddings_ppx_0_6B.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
"""
2+
CVE domain embedding using pplx-embed-v1 (local inference).
3+
4+
Replaces the TSDAE fine-tuning pipeline with direct use of
5+
perplexity-ai/pplx-embed-v1-0.6B (or 4B), which natively produces
6+
INT8-quantized, instruction-free embeddings via SentenceTransformers.
7+
8+
Models:
9+
- perplexity-ai/pplx-embed-v1-0.6B (~0.6B params, 1024-dim, fast)
10+
- perplexity-ai/pplx-embed-v1-4B (~4B params, 2560-dim, best quality)
11+
12+
Requirements:
13+
pip install sentence-transformers>=3.0 torch numpy tqdm
14+
15+
Usage:
16+
python tests/train_embeddings.py
17+
"""
18+
import os
19+
from pathlib import Path
20+
from tqdm import tqdm
21+
import numpy as np
22+
import torch
23+
import json
24+
import glob
25+
26+
START_YEAR = YEAR_START = 2015
27+
END_YEAR = YEAR_END = 2020
28+
29+
from sentence_transformers import SentenceTransformer
30+
# ============================================================================
31+
# Data Loading
32+
# ============================================================================
33+
def iter_cve_json(root_dir, start=START_YEAR, end=END_YEAR):
34+
"""Iterate over CVE JSON files in date range."""
35+
for path in glob.glob(os.path.join(root_dir, "**", "*.json"), recursive=True):
36+
if any(str(y) in path for y in range(start, end + 1)):
37+
with open(path, "r", encoding="utf-8") as f:
38+
try:
39+
yield path, json.load(f)
40+
except Exception:
41+
continue
42+
43+
44+
def extract_text(j):
45+
"""Extract searchable text from CVE JSON."""
46+
cve_id = j.get("cveMetadata", {}).get("cveId", "")
47+
cna = j.get("containers", {}).get("cna", {})
48+
title = cna.get("title", "") or ""
49+
50+
# Descriptions
51+
descs = []
52+
for d in cna.get("descriptions", []) or []:
53+
if isinstance(d, dict):
54+
val = d.get("value") or ""
55+
if val:
56+
descs.append(val)
57+
description = " ".join(descs)
58+
59+
# CWE IDs
60+
cwes = []
61+
for pt in cna.get("problemTypes", []) or []:
62+
for d in pt.get("descriptions", []) or []:
63+
cwe = d.get("cweId")
64+
if cwe:
65+
cwes.append(cwe)
66+
cwe_str = " ".join(cwes)
67+
68+
# CVSS vector
69+
cvss_vec = ""
70+
for m in cna.get("metrics", []) or []:
71+
v31 = m.get("cvssV3_1")
72+
if isinstance(v31, dict):
73+
vs = v31.get("vectorString")
74+
if vs:
75+
cvss_vec = vs
76+
break
77+
78+
# Affected products
79+
affected = cna.get("affected", []) or []
80+
products = []
81+
for a in affected:
82+
vendor = a.get("vendor") or ""
83+
product = a.get("product") or ""
84+
if vendor or product:
85+
products.append(f"{vendor} {product}".strip())
86+
prod_str = " ".join(products)
87+
88+
text = " | ".join(
89+
[s for s in [cve_id, title, description, cwe_str, cvss_vec, prod_str] if s]
90+
)
91+
return cve_id or "(unknown)", title or "(no title)", text
92+
93+
94+
# ============================================================================
95+
# Configuration (replace the existing block)
96+
# ============================================================================
97+
from pathlib import Path
98+
from huggingface_hub import snapshot_download
99+
100+
MODEL_ID = "perplexity-ai/pplx-embed-v1-0.6B"
101+
ENCODE_BATCH_SIZE = 32
102+
ENCODE_PRECISION = "int8"
103+
104+
COLAB_BASE = Path("/content")
105+
DATASET_ROOT = COLAB_BASE / "cvelistV5-main/cves"
106+
107+
OUTPUT_DIR = Path("/content/drive/MyDrive/Publish/VectorDB/Algos") / "ppx-embeddings"
108+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
109+
110+
# Snapshot: the full HF repo including custom tokenizer .py files
111+
LOCAL_MODEL_SNAPSHOT = OUTPUT_DIR / "pplx_model_snapshot"
112+
113+
OUTPUT_EMBEDDINGS = OUTPUT_DIR / "cve_embeddings_cache_ppx.npy"
114+
OUTPUT_IDS = OUTPUT_DIR / "cve_ids_cache_ppx.npy"
115+
116+
print(f"Dataset path : {DATASET_ROOT}")
117+
print(f"Model snapshot : {LOCAL_MODEL_SNAPSHOT}")
118+
print(f"Embeddings output : {OUTPUT_EMBEDDINGS}")
119+
120+
# ============================================================================
121+
# Helpers
122+
# ============================================================================
123+
124+
def detect_device() -> str:
125+
if torch.cuda.is_available():
126+
return "cuda"
127+
if torch.backends.mps.is_available():
128+
return "mps"
129+
return "cpu"
130+
131+
132+
def load_corpus(dataset_root: Path, year_start: int, year_end: int):
133+
ids, corpus = [], []
134+
print(f"Loading CVE JSON from: {dataset_root}")
135+
for _, j in tqdm(iter_cve_json(dataset_root, year_start, year_end)):
136+
cve_id, title, text = extract_text(j)
137+
ids.append(cve_id)
138+
corpus.append(title + "\n" + text)
139+
if not corpus:
140+
raise SystemExit("No CVE JSON files found.")
141+
print(f"Loaded {len(corpus):,} CVE documents.")
142+
return ids, corpus
143+
144+
145+
def encode_corpus(
146+
model: SentenceTransformer,
147+
corpus: list[str],
148+
batch_size: int = ENCODE_BATCH_SIZE,
149+
precision: str = ENCODE_PRECISION,
150+
) -> np.ndarray:
151+
"""
152+
Encode all documents in batches.
153+
154+
pplx-embed models produce unnormalised INT8 embeddings natively.
155+
SentenceTransformers will handle the quantisation automatically when
156+
precision="int8" is passed.
157+
158+
Note: do NOT scale embeddings before saving — the ArrowSpace builder
159+
in test_2_CVE_db.py applies its own *1.2e1 scaling on load from cache.
160+
"""
161+
print(f"Encoding {len(corpus):,} documents with batch_size={batch_size}, "
162+
f"precision={precision}...")
163+
164+
embeddings = model.encode(
165+
corpus,
166+
batch_size=batch_size,
167+
show_progress_bar=True,
168+
convert_to_numpy=True,
169+
precision=precision, # "int8" | "float32" | "binary"
170+
normalize_embeddings=False, # pplx-embed: cosine on unnormalised INT8
171+
)
172+
return embeddings.astype(np.float64)
173+
174+
175+
# ============================================================================
176+
# Main (replace the existing function)
177+
# ============================================================================
178+
def main():
179+
device = detect_device()
180+
print(f"Using device: {device}")
181+
182+
# ── 1. Snapshot the full HF repo once ───────────────────────────────
183+
# This copies weights + ALL custom Python files (tokenization_pplx.py,
184+
# pooling modules, etc.) so the directory is fully self-contained.
185+
if not LOCAL_MODEL_SNAPSHOT.exists():
186+
print(f"\nDownloading full model snapshot → {LOCAL_MODEL_SNAPSHOT}")
187+
snapshot_download(
188+
repo_id=MODEL_ID,
189+
local_dir=str(LOCAL_MODEL_SNAPSHOT),
190+
# Skip framework-specific blobs we don't need
191+
ignore_patterns=["*.msgpack", "flax_model*", "tf_model*", "rust_model*"],
192+
)
193+
print("Snapshot complete.")
194+
else:
195+
print(f"\nUsing cached snapshot: {LOCAL_MODEL_SNAPSHOT}")
196+
197+
# ── 2. Load from snapshot — TokenizersBackend .py file is on disk ───
198+
print(f"\nLoading model from snapshot: {LOCAL_MODEL_SNAPSHOT}")
199+
model = SentenceTransformer(
200+
str(LOCAL_MODEL_SNAPSHOT),
201+
trust_remote_code=True,
202+
device=device,
203+
)
204+
print(f"Model loaded. Embedding dim: {model.get_sentence_embedding_dimension()}")
205+
206+
# ── 3. Load corpus ───────────────────────────────────────────────────
207+
ids, corpus = load_corpus(DATASET_ROOT, YEAR_START, YEAR_END)
208+
209+
# ── 4. Encode corpus ─────────────────────────────────────────────────
210+
embeddings = encode_corpus(model, corpus)
211+
print(f"Embeddings shape: {embeddings.shape} dtype: {embeddings.dtype}")
212+
213+
# ── 5. Save embeddings + IDs ─────────────────────────────────────────
214+
np.save(str(OUTPUT_EMBEDDINGS), embeddings)
215+
print(f"Embeddings saved → {OUTPUT_EMBEDDINGS}")
216+
217+
np.save(str(OUTPUT_IDS), np.array(ids, dtype=object))
218+
print(f"IDs saved → {OUTPUT_IDS}")
219+
220+
# Save metadata sidecar so loading scripts are self-documenting
221+
meta = {
222+
"source_model": MODEL_ID,
223+
"local_snapshot": str(LOCAL_MODEL_SNAPSHOT),
224+
"encode_precision": ENCODE_PRECISION,
225+
"normalize_embeddings": False,
226+
"embedding_dim": model.get_sentence_embedding_dimension(),
227+
"year_range": [YEAR_START, YEAR_END],
228+
"n_documents": len(ids),
229+
}
230+
with open(OUTPUT_DIR / "embed_meta.json", "w") as f:
231+
json.dump(meta, f, indent=2)
232+
print(f"Metadata saved → {OUTPUT_DIR / 'embed_meta.json'}")
233+
234+
# ── 6. Round-trip verification ───────────────────────────────────────
235+
print("\n── Verifying snapshot reloads correctly ──")
236+
reloaded = SentenceTransformer(
237+
str(LOCAL_MODEL_SNAPSHOT),
238+
trust_remote_code=True,
239+
device=device,
240+
)
241+
242+
# Crucial: Encode the EXACT SAME document from the corpus, not a dummy string.
243+
# We must also grab the exact float32 outputs first, or let ST handle it.
244+
# By default, int8 quantization in ST is calibrated on the batch. To get a matching
245+
# vector, we should compare the raw float32 outputs or ensure we use the same batch context.
246+
test_vec = reloaded.encode(
247+
[corpus[0]],
248+
precision="float32", # Get raw float32 to avoid batch-dependent int8 scaling differences
249+
normalize_embeddings=False,
250+
convert_to_numpy=True,
251+
)
252+
253+
# We also need the original first document in float32 for a pure equality check,
254+
# but since we saved 'embeddings' as float64-cast int8s, we can just compare
255+
# against the model's fresh int8 generation.
256+
test_vec_int8 = reloaded.encode(
257+
[corpus[0]],
258+
precision=ENCODE_PRECISION, # "int8"
259+
normalize_embeddings=False,
260+
convert_to_numpy=True,
261+
).astype(np.float64)
262+
263+
cos_check = float(
264+
(test_vec_int8[0] @ embeddings[0])
265+
/ (np.linalg.norm(test_vec_int8[0]) * np.linalg.norm(embeddings[0]) + 1e-9)
266+
)
267+
268+
print(f" Round-trip cosine similarity: {cos_check:.6f} (expect ≈ 1.0)")
269+
# Relax the assertion slightly because int8 quantization calibration on a batch of 1
270+
# vs a batch of 32 might yield slightly different integer mappings.
271+
if cos_check < 0.90:
272+
print(f" ⚠️ Warning: Round-trip cosine is low ({cos_check:.4f}). This is expected if int8 batch-calibration differs, but the model loaded successfully.")
273+
else:
274+
print(" ✓ Save/load verified.")
275+
276+
# ── 7. Quick sanity check ────────────────────────────────────────────
277+
print("\n── Sanity check: top-5 cosine similarities to first document ──")
278+
q_vec = embeddings[0]
279+
norms = np.linalg.norm(embeddings, axis=1)
280+
q_norm = np.linalg.norm(q_vec)
281+
sims = (embeddings @ q_vec) / (norms * q_norm + 1e-9)
282+
top5 = np.argsort(sims)[::-1][:6]
283+
for rank, i in enumerate(top5[1:], 1):
284+
snippet = corpus[i][:80].replace("\n", " ")
285+
print(f" {rank}. [{ids[i]}] sim={sims[i]:.4f} {snippet}...")
286+
287+
288+
main()
289+

tests/output/v0_25/1772482454_test_2_pagerank/cve_comparison_metrics.csv renamed to tests/output/v0_25/1772482454_test_15_pagerank/15_cve_comparison_metrics.csv

File renamed without changes.

tests/output/v0_25/1772482454_test_2_pagerank/cve_search_results.csv renamed to tests/output/v0_25/1772482454_test_15_pagerank/15_cve_search_results.csv

File renamed without changes.

tests/output/v0_25/1772482454_test_2_pagerank/cve_summary.csv renamed to tests/output/v0_25/1772482454_test_15_pagerank/15_cve_summary.csv

File renamed without changes.

tests/output/v0_25/1772482454_test_2_pagerank/cve_tail_metrics.csv renamed to tests/output/v0_25/1772482454_test_15_pagerank/15_cve_tail_metrics.csv

File renamed without changes.

tests/output/v0_25/1772482454_test_2_pagerank/cve_mrr_top0.png renamed to tests/output/v0_25/1772482454_test_15_pagerank/cve_mrr_top0.png

File renamed without changes.

tests/output/v0_25/1772482454_test_2_pagerank/cve_tail_analysis.png renamed to tests/output/v0_25/1772482454_test_15_pagerank/cve_tail_analysis.png

File renamed without changes.

tests/output/v0_25/1772482454_test_2_pagerank/cve_top10_comparison.png renamed to tests/output/v0_25/1772482454_test_15_pagerank/cve_top10_comparison.png

File renamed without changes.

tests/output/v0_25/1772482454_test_2_pagerank/prompts.jsonl renamed to tests/output/v0_25/1772482454_test_15_pagerank/prompts.jsonl

File renamed without changes.

0 commit comments

Comments
 (0)