Skip to content

Commit b1679fb

Browse files
new: Add CNVD severity model comparison validator
Adds a validator script to evaluate the old vs new CNVD severity models side by side on the same deduplicated test set. Reports per-class precision/recall/F1, confusion matrices, and a summary delta table. Related to #19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6352273 commit b1679fb

1 file changed

Lines changed: 146 additions & 0 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import argparse
2+
3+
import numpy as np
4+
import torch
5+
from datasets import concatenate_datasets, load_dataset
6+
from sklearn.metrics import classification_report, confusion_matrix
7+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
8+
9+
from vulntrain.trainers.classify_severity_cnvd import (
10+
SEVERITY_MAPPING,
11+
deduplicate_split,
12+
map_cvss_to_severity,
13+
)
14+
15+
ID2LABEL = {v: k for k, v in SEVERITY_MAPPING.items()}
16+
LABEL2CHINESE = {"Low": "低", "Medium": "中", "High": "高"}
17+
18+
19+
def run_model(model_name, texts, batch_size=64):
20+
"""Run inference and return predicted label indices."""
21+
tokenizer = AutoTokenizer.from_pretrained(model_name)
22+
model = AutoModelForSequenceClassification.from_pretrained(model_name)
23+
model.eval()
24+
25+
all_preds = []
26+
for i in range(0, len(texts), batch_size):
27+
batch_texts = texts[i : i + batch_size]
28+
inputs = tokenizer(
29+
batch_texts, padding=True, truncation=True, max_length=512, return_tensors="pt"
30+
)
31+
with torch.no_grad():
32+
logits = model(**inputs).logits
33+
preds = torch.argmax(logits, dim=-1).tolist()
34+
all_preds.extend(preds)
35+
36+
return np.array(all_preds)
37+
38+
39+
def print_comparison(name, true_labels, preds, label_names):
40+
"""Print classification report and confusion matrix for one model."""
41+
print(f"\n{'=' * 60}")
42+
print(f" {name}")
43+
print(f"{'=' * 60}")
44+
print(
45+
classification_report(
46+
true_labels, preds, target_names=label_names, digits=4, zero_division=0
47+
)
48+
)
49+
print("Confusion matrix (rows=true, cols=predicted):")
50+
print(f"{'':>10}", " ".join(f"{l:>8}" for l in label_names))
51+
cm = confusion_matrix(true_labels, preds, labels=list(range(len(label_names))))
52+
for i, row in enumerate(cm):
53+
print(f"{label_names[i]:>10}", " ".join(f"{v:>8}" for v in row))
54+
print()
55+
56+
57+
def main():
58+
parser = argparse.ArgumentParser(
59+
description="Compare old and new CNVD severity classification models."
60+
)
61+
parser.add_argument(
62+
"--old-model",
63+
dest="old_model",
64+
default="CIRCL/vulnerability-severity-classification-chinese-macbert-base",
65+
help="Old model (before leakage fix / class weighting).",
66+
)
67+
parser.add_argument(
68+
"--new-model",
69+
dest="new_model",
70+
default="CIRCL/vulnerability-severity-classification-chinese-macbert-base-test",
71+
help="New model (after improvements).",
72+
)
73+
parser.add_argument(
74+
"--dataset-id",
75+
dest="dataset_id",
76+
default="CIRCL/Vulnerability-CNVD",
77+
help="HF dataset ID.",
78+
)
79+
parser.add_argument(
80+
"--batch-size",
81+
dest="batch_size",
82+
type=int,
83+
default=64,
84+
help="Inference batch size.",
85+
)
86+
87+
args = parser.parse_args()
88+
89+
# --- Load and prepare a fair (deduplicated) test set ---
90+
print("Loading dataset...")
91+
dataset = load_dataset(args.dataset_id)
92+
93+
# Recombine splits and re-split with deduplication
94+
combined = concatenate_datasets(
95+
[dataset[split] for split in dataset if len(dataset[split]) > 0]
96+
)
97+
combined = combined.map(map_cvss_to_severity)
98+
combined = combined.filter(lambda x: x["severity"] in ["低", "中", "高"])
99+
100+
splits = deduplicate_split(combined, test_size=0.2, seed=42)
101+
test_set = splits["test"]
102+
103+
texts = test_set["description"]
104+
true_labels = np.array(
105+
[SEVERITY_MAPPING[label] for label in test_set["severity_label"]]
106+
)
107+
label_names = [ID2LABEL[i] for i in range(len(SEVERITY_MAPPING))]
108+
109+
print(f"Test set: {len(texts)} samples")
110+
for name, idx in SEVERITY_MAPPING.items():
111+
count = int((true_labels == idx).sum())
112+
print(f" {name}: {count} ({100 * count / len(true_labels):.1f}%)")
113+
114+
# --- Run both models on the same deduplicated test set ---
115+
print(f"\nRunning old model: {args.old_model}")
116+
old_preds = run_model(args.old_model, texts, args.batch_size)
117+
118+
print(f"Running new model: {args.new_model}")
119+
new_preds = run_model(args.new_model, texts, args.batch_size)
120+
121+
# --- Print side-by-side results ---
122+
print_comparison(f"OLD: {args.old_model}", true_labels, old_preds, label_names)
123+
print_comparison(f"NEW: {args.new_model}", true_labels, new_preds, label_names)
124+
125+
# --- Summary delta ---
126+
old_acc = np.mean(old_preds == true_labels)
127+
new_acc = np.mean(new_preds == true_labels)
128+
print("=" * 60)
129+
print(" SUMMARY")
130+
print("=" * 60)
131+
print(f" Overall accuracy: old={old_acc:.4f} new={new_acc:.4f} delta={new_acc - old_acc:+.4f}")
132+
133+
for name, idx in SEVERITY_MAPPING.items():
134+
mask = true_labels == idx
135+
if mask.sum() == 0:
136+
continue
137+
old_recall = np.mean(old_preds[mask] == idx)
138+
new_recall = np.mean(new_preds[mask] == idx)
139+
print(
140+
f" {name:>7} recall: old={old_recall:.4f} new={new_recall:.4f} delta={new_recall - old_recall:+.4f}"
141+
)
142+
print()
143+
144+
145+
if __name__ == "__main__":
146+
main()

0 commit comments

Comments
 (0)