How to train a fraud classifier in Keras

Fraud scoring turns transaction features into a probability that a review system can rank, challenge, or block. A Keras binary classifier is a compact way to test that scoring path when the input is a tabular dataset and the positive class is much smaller than normal transaction traffic.

A synthetic transaction table keeps the run repeatable without customer records. The training code builds numeric risk features, weights the minority class through class_weight, tracks PR-AUC with precision and recall, and saves the trained model as a native .keras file.

Class weights affect the loss during training, while the score threshold controls how many transactions land in a manual review queue. Keep that threshold tied to a holdout set and review capacity; lowering it should raise recall only when the extra false positives are acceptable.

Steps to train a Keras fraud classifier:

  1. Create train_fraud_classifier.py with a synthetic transaction dataset and weighted classifier.
    train_fraud_classifier.py
    import os
    from pathlib import Path
     
    os.environ.setdefault("KERAS_BACKEND", "jax")
     
    import keras
    import numpy as np
     
     
    keras.utils.set_random_seed(7)
    rng = np.random.default_rng(42)
     
    rows = 6000
    amount = rng.lognormal(mean=3.2, sigma=0.9, size=rows).astype("float32")
    hour = rng.integers(0, 24, size=rows).astype("float32")
    country_risk = rng.beta(1.2, 8.0, size=rows).astype("float32")
    merchant_risk = rng.beta(1.4, 7.0, size=rows).astype("float32")
    device_age_days = rng.exponential(scale=20.0, size=rows).astype("float32")
    chargeback_count = rng.poisson(0.12, size=rows).astype("float32")
    night_purchase = ((hour < 6) | (hour > 22)).astype("float32")
     
    amount_log = np.log1p(amount)
    amount_z = (amount_log - amount_log.mean()) / amount_log.std()
    fraud_score = (
        -5.5
        + 1.2 * amount_z
        + 6.4 * country_risk
        + 5.7 * merchant_risk
        + 1.2 * night_purchase
        + 1.6 * (device_age_days < 2)
        + 1.5 * (chargeback_count > 0)
    )
    fraud_probability = 1.0 / (1.0 + np.exp(-fraud_score))
    target = rng.binomial(1, fraud_probability).astype("float32")
     
    features = np.column_stack(
        [
            amount_log,
            hour / 23.0,
            country_risk,
            merchant_risk,
            np.log1p(device_age_days),
            chargeback_count,
            night_purchase,
        ]
    ).astype("float32")
     
    order = rng.permutation(rows)
    train_size = int(rows * 0.8)
    train_index = order[:train_size]
    validation_index = order[train_size:]
    x_train = features[train_index]
    y_train = target[train_index]
    x_validation = features[validation_index]
    y_validation = target[validation_index]
     
    negative = float((y_train == 0).sum())
    positive = float((y_train == 1).sum())
    total = negative + positive
    class_weight = {
        0: total / (2.0 * negative),
        1: total / (2.0 * positive),
    }
     
    normalizer = keras.layers.Normalization()
    normalizer.adapt(x_train)
     
    model = keras.Sequential(
        [
            keras.layers.Input(shape=(x_train.shape[1],)),
            normalizer,
            keras.layers.Dense(32, activation="relu"),
            keras.layers.Dropout(0.2),
            keras.layers.Dense(16, activation="relu"),
            keras.layers.Dense(1, activation="sigmoid"),
        ]
    )
     
    model.compile(
        optimizer=keras.optimizers.Adam(learning_rate=0.005),
        loss=keras.losses.BinaryCrossentropy(),
        metrics=[
            keras.metrics.AUC(curve="PR", name="pr_auc"),
            keras.metrics.Precision(name="precision"),
            keras.metrics.Recall(name="recall"),
        ],
        jit_compile="auto",
    )
     
    history = model.fit(
        x_train,
        y_train,
        validation_data=(x_validation, y_validation),
        epochs=8,
        batch_size=128,
        class_weight=class_weight,
        verbose=0,
    )
     
    validation_probability = model.predict(
        x_validation,
        batch_size=256,
        verbose=0,
    ).ravel()
    threshold = 0.35
    validation_prediction = validation_probability >= threshold
    actual_positive = y_validation == 1
    actual_negative = y_validation == 0
    true_positive = int(np.logical_and(validation_prediction, actual_positive).sum())
    false_positive = int(np.logical_and(validation_prediction, actual_negative).sum())
    false_negative = int(np.logical_and(~validation_prediction, actual_positive).sum())
    true_negative = int(np.logical_and(~validation_prediction, actual_negative).sum())
    threshold_precision = true_positive / max(true_positive + false_positive, 1)
    threshold_recall = true_positive / max(true_positive + false_negative, 1)
     
    model_path = Path("fraud_classifier.keras")
    model.save(model_path, overwrite=True)
    loaded_model = keras.saving.load_model(model_path)
    sample_transactions = np.array(
        [
            [np.log1p(38.20), 14 / 23.0, 0.04, 0.08, np.log1p(120.0), 0.0, 0.0],
            [np.log1p(620.00), 2 / 23.0, 0.54, 0.61, np.log1p(0.5), 2.0, 1.0],
        ],
        dtype="float32",
    )
    sample_scores = loaded_model.predict(sample_transactions, verbose=0).ravel()
     
    final_epoch = len(history.history["loss"])
    final_val_pr_auc = history.history["val_pr_auc"][-1]
    final_val_precision = history.history["val_precision"][-1]
    final_val_recall = history.history["val_recall"][-1]
     
    print(f"backend: {keras.config.backend()}")
    print(f"train rows: {len(x_train)}")
    print(f"validation rows: {len(x_validation)}")
    print(f"training fraud rate: {y_train.mean():.3f}")
    print(f"class weights: legit={class_weight[0]:.2f}, fraud={class_weight[1]:.2f}")
    print(f"epochs completed: {final_epoch}")
    print(f"val pr_auc: {final_val_pr_auc:.3f}")
    print(f"val precision@0.5: {final_val_precision:.3f}")
    print(f"val recall@0.5: {final_val_recall:.3f}")
    print(
        f"threshold 0.35 counts: tp={true_positive}, "
        f"fp={false_positive}, fn={false_negative}, tn={true_negative}"
    )
    print(
        "threshold 0.35 metrics: "
        f"precision={threshold_precision:.3f}, recall={threshold_recall:.3f}"
    )
    print(f"saved model: {model_path.name}")
    print(
        "sample fraud scores: "
        f"routine={sample_scores[0]:.3f}, suspicious={sample_scores[1]:.3f}"
    )

    The KERAS_BACKEND assignment must happen before import keras. Install Keras and the selected backend in the project environment before running the script.
    Related: How to install Keras with pip
    Related: How to set the Keras backend
    Related: How to compile a model in Keras

  2. Run the training script.
    $ python train_fraud_classifier.py
    backend: jax
    train rows: 4800
    validation rows: 1200
    training fraud rate: 0.115
    class weights: legit=0.56, fraud=4.35
    epochs completed: 8
    val pr_auc: 0.425
    val precision@0.5: 0.301
    val recall@0.5: 0.743
    threshold 0.35 counts: tp=118, fp=349, fn=22, tn=711
    threshold 0.35 metrics: precision=0.253, recall=0.843
    saved model: fraud_classifier.keras
    sample fraud scores: routine=0.050, suspicious=1.000
  3. Compare the default metric threshold with the lower review threshold.
    val precision@0.5: 0.301
    val recall@0.5: 0.743
    threshold 0.35 counts: tp=118, fp=349, fn=22, tn=711
    threshold 0.35 metrics: precision=0.253, recall=0.843

    The Precision and Recall metric objects report at the default 0.5 threshold. The separate threshold block shows the effect of a lower cutoff for manual review.

  4. Confirm that the saved model and scored transactions appear in the output.
    saved model: fraud_classifier.keras
    sample fraud scores: routine=0.050, suspicious=1.000

    The Normalization layer is stored inside the .keras model, but the scoring code must keep the same feature order used during training.
    Related: How to save and load a Keras model
    Related: How to run batch prediction in Keras