Training Fraud Models with Adversarial Synthetic Data
VynFi 3.0's adversarial mode takes an ONNX model, probes its decision boundary, and generates targeted synthetic examples where the model is least confident. This post covers the full pipeline from model upload to augmented retraining.
Fraud detection models degrade in production for a predictable reason: adversaries adapt. The transactions that slip through your classifier today are precisely the ones that sit near its decision boundary — the region where the model is least confident and most vulnerable. Traditional augmentation (SMOTE, random oversampling) generates synthetic examples uniformly across the feature space, which wastes training budget on regions the model already handles well.
VynFi 3.0's adversarial mode flips this approach. You supply a trained model in ONNX format, and the engine probes its decision surface to identify boundary regions. It then generates synthetic financial transactions specifically in those regions — transactions that are structurally valid (balanced entries, proper document flows, Benford-compliant amounts) but sit exactly where the model struggles.
**DataSynth 3.1.1 update:** Adversarial training now pairs cleanly with the scheme-vs-direct fraud split. `is_fraud_propagated = true` entries (document ring fan-out) are structurally different from direct line-level injections, and the behavioural biases (weekend ×32, round-dollar ×170, post-close ×3,106 lift) ensure both populations carry real discriminative signal. Train two-stage detectors: generic boundary probe + propagation-stratified retrain via `client.jobs.fraud_split(job_id)`. See adversarial_fraud_training.py.
How Boundary Probing Works
The adversarial engine performs three steps. First, it runs a grid of synthetic examples through the ONNX model and collects prediction probabilities. Second, it identifies the decision boundary by finding the manifold where P(fraud) is close to 0.5 — the region of maximum uncertainty. Third, it uses a constrained sampling strategy to generate new examples concentrated around that boundary, subject to the structural validity constraints of the financial domain (entries must balance, dates must be sequential, reference chains must be intact).
The <code>boundary_sigma</code> parameter controls how tightly the generated examples cluster around the decision boundary. A smaller sigma produces examples very close to the boundary (harder cases); a larger sigma produces a broader spread (more coverage of the uncertain region).
Uploading a Model and Generating Adversarial Data
import vynficlient = vynfi.VynFi()# Upload an ONNX fraud classifiermodel = client.models.upload( path="fraud_classifier_v7.onnx", name="fraud_classifier_v7", task="binary_classification", target_class="fraud", feature_schema={ "amount": "float", "hour_of_day": "int", "is_weekend": "bool", "counterparty_risk_score": "float", "days_since_account_open": "int", "transaction_frequency_7d": "float", },)# Generate adversarial examples near the decision boundaryjob = client.jobs.create( mode="adversarial", model_id=model.id, n_samples=10_000, boundary_sigma=0.05, sector="banking", constraints={"balanced_entries": True, "benford_compliance": True},)result = client.jobs.wait(job.id)archive = client.jobs.download_archive(result.id)Analyzing the Boundary Region
The output includes a <code>boundary_analysis.json</code> file that describes the decision boundary geometry: which features contribute most to boundary proximity, cluster centers in the uncertain region, and the model's calibration curve across the generated samples. This analysis is useful even if you do not retrain — it tells you where your model is weakest.
import pandas as pdimport json# Load adversarial samples and boundary analysisadversarial_df = pd.read_parquet(archive.file("adversarial_samples.parquet"))analysis = json.loads(archive.text("boundary_analysis.json"))print(f"Generated {len(adversarial_df)} adversarial samples")print(f"Mean model confidence: {adversarial_df['model_prob'].mean():.3f}")print(f"Samples within 0.1 of boundary: {(adversarial_df['model_prob'].between(0.4, 0.6)).sum()}")# Top features driving boundary proximityfor feat in analysis["boundary_features"][:5]: print(f" {feat['name']}: importance={feat['importance']:.3f}, range=[{feat['low']:.2f}, {feat['high']:.2f}]")Retraining with Augmented Data
from sklearn.ensemble import GradientBoostingClassifierfrom sklearn.metrics import precision_recall_fscore_supportimport numpy as np# Combine original training data with adversarial augmentationoriginal_train = pd.read_parquet("training_data.parquet")augmented = pd.concat([original_train, adversarial_df], ignore_index=True)features = ["amount", "hour_of_day", "is_weekend", "counterparty_risk_score", "days_since_account_open", "transaction_frequency_7d"]X_aug = augmented[features].valuesy_aug = augmented["label"].values# Train on augmented datamodel_v8 = GradientBoostingClassifier(n_estimators=500, max_depth=6)model_v8.fit(X_aug, y_aug)# Evaluate on held-out test setX_test = pd.read_parquet("test_data.parquet")[features].valuesy_test = pd.read_parquet("test_data.parquet")["label"].valuesy_pred = model_v8.predict(X_test)p, r, f1, _ = precision_recall_fscore_support(y_test, y_pred, pos_label=1, average="binary")print(f"Precision: {p:.3f}, Recall: {r:.3f}, F1: {f1:.3f}")In internal benchmarks, adversarial augmentation improved F1 scores on held-out boundary-region test sets by 12-18% compared to uniform SMOTE augmentation, while maintaining equivalent performance on the non-boundary majority of the distribution. The key advantage is efficiency: rather than generating millions of random synthetic examples and hoping some land near the boundary, adversarial mode targets precisely the region that matters.