Skip to main content

Ictonyx: Iteration Comparison Testing Over N-runs: Yield eXamination

Project description

Ictonyx

Iteration Comparison Testing Over N-runs: Yield eXamination

A Python framework for studying machine learning model variability and performing rigorous statistical comparisons.

CI/CD PyPI Python License Open In Colab


The problem

Training a neural network involves stochastic factors: random weight initialisation, data shuffling, dropout. Train the same architecture on the same dataset twice and you will get different weights, different predictions, and different evaluation metrics.

This means a model's performance is a random variable, not a constant — and treating it as a constant, as most practitioners do, leads to conclusions that cannot be replicated or trusted. Reporting a single accuracy from a single training run is not a measurement; it is a sample of size one.

Ictonyx trains a model N times under independent random seeds, collects the full distribution of outcomes, and provides the statistical machinery to reason about that distribution rigorously.


Installation

# scikit-learn (no deep learning)
pip install ictonyx[sklearn]

# TensorFlow / Keras
pip install ictonyx[tensorflow]

# PyTorch
pip install ictonyx[torch]

# Everything
pip install ictonyx[all]

# Process isolation for long GPU runs (recommended with tensorflow or torch)
pip install "ictonyx[tensorflow,isolation]"

Requires Python 3.10+. Current release: 0.3.15changelog · PyPI

Quick start

Train a small CNN on CIFAR-10 twenty times and observe the distribution of outcomes. The task is genuinely difficult — 10 classes, colour images, a non-convex loss landscape — so initialisation matters and the spread across runs is real.

import numpy as np
import tensorflow as tf
import ictonyx as ix
from ictonyx import ModelConfig, KerasModelWrapper, ArraysDataHandler, run_variability_study

# Load CIFAR-10 — downloads once to ~/.keras/datasets/, cached permanently after
(X_train, y_train), _ = tf.keras.datasets.cifar10.load_data()
X = (X_train[:20000].astype('float32') / 255.0)
y = y_train[:20000].flatten()


def build_cnn(config: ModelConfig) -> KerasModelWrapper:
    model = tf.keras.Sequential([
        tf.keras.Input(shape=(32, 32, 3)),
        tf.keras.layers.Conv2D(32, (3, 3), activation='relu', padding='same'),
        tf.keras.layers.MaxPooling2D((2, 2)),
        tf.keras.layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
        tf.keras.layers.MaxPooling2D((2, 2)),
        tf.keras.layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(128, activation='relu'),
        tf.keras.layers.Dropout(0.4),
        tf.keras.layers.Dense(10, activation='softmax'),
    ])
    model.compile(
        optimizer='adam',
        loss='sparse_categorical_crossentropy',
        metrics=['accuracy'],
    )
    return KerasModelWrapper(model)


config = ModelConfig({'epochs': 15, 'batch_size': 64, 'verbose': 0})
data_handler = ArraysDataHandler(X, y)

results = run_variability_study(
    model_builder=build_cnn,
    data_handler=data_handler,
    model_config=config,
    num_runs=20,
    seed=42,
)

print(results.summarize())
Variability Study Results
==============================
Successful runs: 20
Seed: 42
train_accuracy:
  Mean: 0.8198
  SD (sample, N-1):  0.0182
  Min:  0.7869
  Max:  0.8491
train_loss:
  Mean: 0.4975
  SD (sample, N-1):  0.0489
  Min:  0.4177
  Max:  0.5945
val_accuracy:
  Mean: 0.6448
  SD (sample, N-1):  0.0101
  Min:  0.6260
  Max:  0.6615
val_loss:
  Mean: 1.1925
  SD (sample, N-1):  0.0523
  Min:  1.1163
  Max:  1.2751

Here we have a 3.7 percent range for validation accuracy across twenty runs of the same architecture on the same data. This represents a source of variability that shouldn't be neglected.

ix.plot_variability_summary(
    all_runs_metrics_list=results.all_runs_metrics,
    final_metrics_series=results.final_metrics['val_accuracy'],
    metric='accuracy',
)

Variability summary for CIFAR-10 CNN across 10 runs


Comparing two architectures

A common question in deep learning: does adding depth or regularisation actually help, or does it just happen to get a better seed? Ictonyx answers this by running a full variability study for each architecture and applying a statistical test to the resulting distributions.

def build_dense(config: ModelConfig) -> KerasModelWrapper:
    """A simpler dense baseline — no convolutions."""
    model = tf.keras.Sequential([
        tf.keras.Input(shape=(32, 32, 3)),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(256, activation='relu'),
        tf.keras.layers.Dropout(0.4),
        tf.keras.layers.Dense(128, activation='relu'),
        tf.keras.layers.Dense(10, activation='softmax'),
    ])
    model.compile(
        optimizer='adam',
        loss='sparse_categorical_crossentropy',
        metrics=['accuracy'],
    )
    return KerasModelWrapper(model)


comparison = ix.compare_models(
    models=[build_cnn, build_dense],
    data=data_handler,
    runs=20,
    metric='val_accuracy',
    seed=42,
)

print(comparison.get_summary())
Model Comparison Results (val_accuracy)
========================================
Models compared: 2
Omnibus test: Kruskal-Wallis H-Test: 29.279, p=0.0000 ***, epsilon-squared=0.744

Pairwise comparisons (holm correction):
  build_cnn_vs_build_dense: Mann-Whitney U Test: 400.000, p_corr=0.0000 ***, rank-biserial correlation=1.000 *

Significant pairs: build_cnn_vs_build_dense

A rank-biserial correlation of 1.0 means the CNN outperformed the dense network in every single one of the ten paired runs. The p-value establishes that this is not attributable to chance; the effect size establishes that it is absolute.


Process isolation for GPU runs

Keras models accumulate GPU memory across training runs in a loop. For studies with many runs or large models, run each training session in a subprocess:

results = run_variability_study(
    model_builder=build_cnn,
    data_handler=data_handler,
    model_config=config,
    num_runs=20,
    use_process_isolation=True,
    gpu_memory_limit=4096,
    seed=42,
)

Each run executes in a child process and exits cleanly, releasing all GPU memory before the next run begins.


scikit-learn

Ictonyx works equally well with sklearn estimators. Pass a class and Ictonyx constructs a fresh instance for each run; pass a configured instance and Ictonyx clones it per run via sklearn.base.clone().

import ictonyx as ix
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.datasets import load_breast_cancer
import pandas as pd

data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df['target'] = data.target

comparison = ix.compare_models(
    models=[RandomForestClassifier, GradientBoostingClassifier],
    data=df,
    target_column='target',
    runs=20,
    metric='val_accuracy',
    seed=42,
)

print(comparison.get_summary())
Model Comparison Results (val_accuracy)
========================================
Models compared: 2
Omnibus test: Kruskal-Wallis H-Test: 2.053, p=0.1519 ns, epsilon-squared=0.028

No pairwise comparisons performed (omnibus test not significant).

PyTorch

import torch
import torch.nn as nn
import ictonyx as ix
from ictonyx import ModelConfig, PyTorchModelWrapper, ArraysDataHandler, run_variability_study

def build_net(config: ModelConfig) -> PyTorchModelWrapper:
    model = nn.Sequential(
        nn.Linear(30, 64), nn.ReLU(),
        nn.Linear(64, 32), nn.ReLU(),
        nn.Linear(32, 2),
    )
    return PyTorchModelWrapper(
        model,
        criterion=nn.CrossEntropyLoss(),
        optimizer_class=torch.optim.Adam,
        optimizer_params={'lr': config.get('learning_rate', 0.001)},
        task='classification',
    )

# Pass arrays directly — Ictonyx handles splitting
import numpy as np
from sklearn.datasets import load_breast_cancer
data = load_breast_cancer()
X = data.data.astype(np.float32)
y = data.target.astype(np.int64)

results = run_variability_study(
    model_builder=build_net,
    data_handler=ArraysDataHandler(X, y, val_split=0.2, test_split=0.1),
    model_config=ModelConfig({'epochs': 30, 'batch_size': 32, 'learning_rate': 0.001}),
    num_runs=20,
    seed=42,
)

ix.plot_variability_summary(
    all_runs_metrics_list=results.all_runs_metrics,
    final_metrics_series=results.final_metrics['val_accuracy'],
    metric='accuracy',
)

Variability summary for PyTorch classifier across 20 runs


Working with results

# Full distribution of any metric across runs
results.get_metric_values('val_accuracy')       # List[float]

# Per-epoch statistics across all runs
results.get_epoch_statistics('val_accuracy')    # DataFrame: epoch, mean, sd, se, ci_lower, ci_upper

# All per-run, per-epoch DataFrames
results.all_runs_metrics                        # List[pd.DataFrame]

# Seed for exact reproducibility
results.seed

Examples

The examples/ directory contains Jupyter notebooks:

  • quickstart.ipynb — CIFAR-10 variability study and two-architecture comparison
  • 01_cifar10_variability_study.ipynb — deep dive into CNN variability with full visualisation
  • 02_cifar10_model_comparison.ipynb — comparing architectures statistically
  • 03_learning_rate_variability.ipynb — grid study across learning rates and batch sizes
  • 04_pytorch_classification.ipynb — PyTorch classification workflow
  • 05_pytorch_regression.ipynb — PyTorch regression workflow
  • 06_sklearn_models.ipynb — comparing multiple sklearn classifiers

License

MIT. See LICENSE.


Citation

If you use Ictonyx in published work, please cite it using the metadata in CITATION.cff, or use the Cite this repository button on the GitHub repository page.

@software{kizlik_ictonyx,
  author  = {Kizlik, Stephen},
  title   = {Ictonyx: A Framework for Variability Analysis in Machine Learning Training},
  version = {0.3.15},
  url     = {https://github.com/skizlik/ictonyx},
  license = {MIT},
}

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ictonyx-0.3.15.tar.gz (111.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ictonyx-0.3.15-py3-none-any.whl (115.5 kB view details)

Uploaded Python 3

File details

Details for the file ictonyx-0.3.15.tar.gz.

File metadata

  • Download URL: ictonyx-0.3.15.tar.gz
  • Upload date:
  • Size: 111.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.2 CPython/3.12.3 Linux/6.11.0-29-generic

File hashes

Hashes for ictonyx-0.3.15.tar.gz
Algorithm Hash digest
SHA256 f49978f9407da5e350d34eaf8ea4a33dfe43d2f471597d3957243a6aa339486c
MD5 6fa1281b9b21e5d926c231b48f9bce53
BLAKE2b-256 ab9d8fc0ac2e0facbfd5c56a55510291301c4730abc0e01a36b20150a3f5245f

See more details on using hashes here.

File details

Details for the file ictonyx-0.3.15-py3-none-any.whl.

File metadata

  • Download URL: ictonyx-0.3.15-py3-none-any.whl
  • Upload date:
  • Size: 115.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.2 CPython/3.12.3 Linux/6.11.0-29-generic

File hashes

Hashes for ictonyx-0.3.15-py3-none-any.whl
Algorithm Hash digest
SHA256 e6cef02fb38f34216c54688fb52a4a68e7e79e2b76b9f58194427645a46e369d
MD5 b55263e657c4011c26776d51d387f71b
BLAKE2b-256 02763ca15b6273a0950f02b0d706c5826999740391c393d94c9648366fb2a965

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page