ctokx's picture
Add src/
468c4c2 verified
Raw
History Blame Contribute Delete
3.08 kB
"""Non-neural baselines.
These exist to make the fine-tuned encoder falsifiable. A large share of
published cyber-ML results do not clear a TF-IDF + logistic regression floor,
and a model that fails to beat these has no reason to be downloaded.
* ``frequency`` — always predict the k most common training techniques.
Establishes what a model that has learned nothing about the input scores.
* ``keyword`` — substring match on ATT&CK technique names. Zero training.
* ``tfidf_lr`` — TF-IDF word+char n-grams, one-vs-rest logistic regression.
The real floor. Usually much stronger than people expect.
"""
from __future__ import annotations
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import FeatureUnion
from . import attack_meta
def frequency_scores(train_Y: np.ndarray, n_eval: int) -> np.ndarray:
"""Score every technique by its training prevalence, identically for all rows."""
prior = train_Y.mean(axis=0)
return np.tile(prior, (n_eval, 1))
def keyword_scores(texts: list[str], labels: list[str]) -> np.ndarray:
"""1.0 where the technique's name appears verbatim in the sentence, else 0."""
kw = attack_meta.technique_keywords(labels)
scores = np.zeros((len(texts), len(labels)), dtype=np.float32)
lowered = [t.lower() for t in texts]
for j, tid in enumerate(labels):
forms = kw.get(tid, [])
if not forms:
continue
for i, text in enumerate(lowered):
if any(f in text for f in forms):
scores[i, j] = 1.0
return scores
def _vectorizer() -> FeatureUnion:
return FeatureUnion([
("word", TfidfVectorizer(
ngram_range=(1, 2), min_df=2, sublinear_tf=True,
strip_accents="unicode", lowercase=True)),
("char", TfidfVectorizer(
analyzer="char_wb", ngram_range=(3, 5), min_df=3, sublinear_tf=True,
lowercase=True)),
])
def tfidf_lr_scores(
train_texts: list[str], train_Y: np.ndarray, eval_sets: dict[str, list[str]]
) -> dict[str, np.ndarray]:
"""One-vs-rest logistic regression over TF-IDF word + character n-grams.
Classes with a single training polarity (all-negative after splitting) are
handled explicitly rather than allowed to raise, and score a constant 0.
"""
vec = _vectorizer()
Xtr = vec.fit_transform(train_texts)
Xev = {name: vec.transform(texts) for name, texts in eval_sets.items()}
out = {name: np.zeros((X.shape[0], train_Y.shape[1]), dtype=np.float32)
for name, X in Xev.items()}
for j in range(train_Y.shape[1]):
y = train_Y[:, j]
if y.sum() == 0 or y.sum() == len(y):
continue # degenerate class: leave scores at 0
clf = LogisticRegression(
C=4.0, max_iter=2000, class_weight="balanced", solver="liblinear")
clf.fit(Xtr, y)
for name, X in Xev.items():
out[name][:, j] = clf.predict_proba(X)[:, 1]
return out