aiml
SearchMy learning
Menu

Lesson 12 of 19Module 4

Gate a model candidate on validation results

Make a quality decision about the exact exported candidate using explicit count-based checks.

Make a quality decision about the exact exported candidate using explicit count-based checks. A local Rice-classifier lab with explicit failure checks.

Prerequisites

Export a full-precision inference bundle; confusion matrices, accuracy and per-class recall.

Preparation and term definitions

By the end, you can

  • Bind validation predictions, membership and policy to an exact candidate digest.
  • Derive accuracy and per-class recall decisions from confusion counts.
  • Refuse a structurally valid candidate that fails the declared quality policy.
Runtime
Python 3.12+
Dependencies
Standard library only

Lab code, tests and fixtures (ZIP, 2.4 MB)Quiz check your understanding

Progress is saved in this browser.

Problem

When you need this. Apply a quality gate when successful training alone cannot decide whether a candidate meets the declared validation requirements.

A training process can exit successfully and emit valid parameters while still failing a task-quality requirement. After exporting the exact inference bundle, you will apply a declared validation policy to those bytes and inspect a candidate that must be refused.

Concepts

A quality gate makes a pass/fail decision from task outcomes and a declared policy. It is separate from successful process exit, valid JSON and matching artifact identity. This exercise requires validation accuracy of at least 90% and recall of at least 85% for each Rice class. The values are explicit exercise policy, not inferred business requirements.

Accuracy divides all correct classifications by all validation rows. Recall for Cammeo divides correctly predicted Cammeo rows by all actual Cammeo rows; Osmancik recall uses the other actual-class row. A majority-class predictor can therefore have acceptable aggregate accuracy in some datasets while failing a class-recall requirement.

Architecture

Expected digest +candidate bytesVerify identity +predict validationCount outcomes +apply fixed policyBound validationgate resultExact validationmembershiprowsFailed qualitypolicyrefuseExpected digest +candidate bytesVerify identity +predict validationCount outcomes +apply fixed policyBound validationgate resultExact validationmembershipFailed qualitypolicy
The selected candidate predicts the validation rows. Exact confusion counts are checked against accuracy and per-class recall thresholds. A failed result remains a refusal to register.

The gate verifies the expected candidate digest and supported training identity, predicts each validation row with the reloaded bundle, and accumulates confusion counts. It binds the candidate digest, validation membership and policy into its result. Registration recomputes this gate rather than accepting a caller-edited passed flag.

Working code

Download the complete lab, extract it, and enter mlops-registry-serving/. Use Python 3.12 or later; there are no third-party dependencies. The ZIP includes the candidate, data, source and tests.

Run the focused gate exercise and its regression testsTerminal · bash
python exercise.py gate
python -m unittest -v test_pipeline.py

Derive the saved decision from its counts

Run python exercise.py gate. It recomputes validation predictions for the saved candidate and then evaluates a deliberately constant-class copy. No training occurs in this exercise.

The candidate gets 547 of 590 rows correct. Cammeo recall is 223 / 248; Osmancik recall is 324 / 342. Compare each fraction with the policy, rather than using a rounded percentage printed for display.

The implementation checks correct × denominator >= total × numerator, using the policy’s integer ratios. For example, the accuracy requirement uses the ratio 90/100. This avoids accepting a candidate because a displayed rounded score happens to reach the threshold.

Refuse a valid but inadequate classifier

The exercise sets every weight to zero and the bias to a positive constant. That copy predicts Cammeo for every row. It remains a structurally valid model but loses all Osmancik recall. Inspect constant_class_candidate, its false gate result and the registration refusal. The failed attempt leaves 0 files in the temporary registry.

Actual gate exercise report from the previously trained Rice candidate, including its recorded result and refusal case.
Actual local execution using a previously trained candidate. This exercise does not perform new training. Full-resolution capture · Raw result · Command and hashes.
Read the actual pipeline.py implementation
pipeline.pySource · pythonDownload
"""Local Rice inference export, validation gate and digest-selected registry.

No external packages. This is a trusted local filesystem exercise, not MLflow.
"""
import hashlib
import json
import math
import re
from pathlib import Path
from vendor import baseline

HERE = Path(__file__).resolve().parent
CONTRACT = 'rice-measurements-v1'
POLICY = {'accuracy': [90, 100], 'class_recall': [85, 100]}


def encode(value):
    return (json.dumps(value, sort_keys=True, separators=(',', ':'),
                       allow_nan=False) + '\n').encode()


def digest(data):
    return hashlib.sha256(data).hexdigest()


def pairs(items):
    result = {}
    for key, value in items:
        if key in result:
            raise ValueError('duplicate JSON key')
        result[key] = value
    return result


def decode(data):
    return json.loads(data, object_pairs_hook=pairs,
                      parse_constant=lambda _: (_ for _ in ()).throw(ValueError('nonfinite JSON')))


def number(value):
    try:
        finite = type(value) in (float, int) and math.isfinite(value)
    except OverflowError:
        finite = False
    if not finite:
        raise ValueError('expected a finite non-Boolean number')
    return value


def validate(bundle):
    if not isinstance(bundle, dict):
        raise ValueError('expected a bundle object')
    if (type(bundle.get('format')) is not int or bundle.get('format') != 1 or bundle.get('contract') != CONTRACT
            or bundle.get('features') != baseline.FEATURES
            or bundle.get('labels') != baseline.LABELS
            or bundle.get('positive') != baseline.POSITIVE):
        raise ValueError('unsupported bundle contract')
    for field in ('weights', 'mean', 'std'):
        values = bundle.get(field)
        if not isinstance(values, list) or len(values) != len(baseline.FEATURES):
            raise ValueError('wrong parameter dimensions')
        for value in values:
            number(value)
    if any(value <= 0 for value in bundle['std']):
        raise ValueError('nonpositive standard deviation')
    number(bundle.get('bias'))
    return bundle


def predict(bundle, request, version):
    if not isinstance(request, dict) or set(request) != {'contract', 'features'}:
        raise ValueError('expected contract and features')
    features = request['features']
    if (request['contract'] != bundle['contract'] or not isinstance(features, dict)
            or set(features) != set(bundle['features'])):
        raise ValueError('feature contract mismatch')
    values = [number(features[name]) for name in bundle['features']]
    scaled = [(v - m) / s for v, m, s in zip(values, bundle['mean'], bundle['std'])]
    z = bundle['bias'] + sum(w * v for w, v in zip(bundle['weights'], scaled))
    if not all(math.isfinite(v) for v in scaled) or not math.isfinite(z):
        raise ValueError('nonfinite computation')
    probability = baseline.sigmoid(z)
    label = baseline.POSITIVE if z >= 0 else baseline.LABELS[1]
    return {'version': version, 'contract': CONTRACT, 'label': label,
            'positive_probability': probability}


def request_for(row):
    return {'contract': CONTRACT, 'features': dict(zip(baseline.FEATURES, row['x']))}


def export():
    parts = baseline.split_rows(baseline.load_rows())
    scaler = baseline.fit_scaler(parts['training'])
    model = baseline.train_logistic(parts['training'], scaler)
    bundle = {'format': 1, 'contract': CONTRACT, 'features': baseline.FEATURES,
              'labels': baseline.LABELS, 'positive': baseline.POSITIVE,
              'weights': model['weights'], 'bias': model['bias'], **scaler,
              'training': {'dataset_sha256': baseline.DATA_SHA256,
                           'split_salt': baseline.SALT, 'epochs': baseline.EPOCHS,
                           'learning_rate': baseline.LEARNING_RATE,
                           'trainer_sha256': digest(Path(baseline.__file__).read_bytes()),
                           'exporter_sha256': digest(Path(__file__).read_bytes())}}
    validate(bundle)
    # Match the source predictor on every validation row before retaining bytes.
    version = digest(encode(bundle))
    for row in parts['validation']:
        if predict(bundle, request_for(row), version)['label'] != baseline.predict_logistic(model, scaler, row):
            raise ValueError('export prediction mismatch')
    return encode(bundle)


def quality_gate(data, expected):
    if digest(data) != expected:
        raise ValueError('candidate digest mismatch')
    bundle = validate(decode(data))
    training = bundle.get('training', {})
    required = {'dataset_sha256': baseline.DATA_SHA256, 'split_salt': baseline.SALT,
                'epochs': baseline.EPOCHS, 'learning_rate': baseline.LEARNING_RATE,
                'trainer_sha256': digest(Path(baseline.__file__).read_bytes()),
                'exporter_sha256': digest(Path(__file__).read_bytes())}
    if training != required:
        raise ValueError('unsupported training identity')
    rows = baseline.split_rows(baseline.load_rows())['validation']
    matrix = {label: {other: 0 for other in baseline.LABELS} for label in baseline.LABELS}
    for row in rows:
        result = predict(bundle, request_for(row), expected)
        matrix[row['label']][result['label']] += 1
    correct = sum(matrix[label][label] for label in matrix)
    enough = lambda a, b, ratio: b > 0 and a * ratio[1] >= b * ratio[0]
    passed = enough(correct, len(rows), POLICY['accuracy']) and all(
        enough(matrix[label][label], sum(matrix[label].values()), POLICY['class_recall'])
        for label in matrix)
    return {'candidate_sha256': expected, 'partition': 'validation', 'policy': POLICY,
            'membership_sha256': digest(encode(sorted(row['key'] for row in rows))),
            'rows': len(rows), 'correct': correct, 'confusion': matrix, 'passed': passed}


def registered_file(root, version):
    if not isinstance(version, str) or not re.fullmatch('[0-9a-f]{64}', version):
        raise ValueError('select an explicit SHA-256 version')
    root = Path(root)
    if root.is_symlink():
        raise ValueError('symlink registry root')
    return root / (version + '.json')


def register(root, data, expected):
    record = quality_gate(data, expected)
    if not record['passed']:
        raise ValueError('candidate failed quality policy')
    path = registered_file(root, expected)
    path.parent.mkdir(parents=True, exist_ok=True)
    if path.is_symlink():
        raise ValueError('symlink registration')
    try:
        with path.open('xb') as stream:
            stream.write(data)
    except FileExistsError:
        if path.read_bytes() != data:
            raise ValueError('conflicting or incomplete registration') from None
    return record


def load(root, expected):
    path = registered_file(root, expected)
    if path.is_symlink():
        raise ValueError('symlink registration')
    data = path.read_bytes()
    if digest(data) != expected:
        raise ValueError('registered bytes changed or incomplete')
    return validate(decode(data))


def batch(bundle, requests, version):
    if not isinstance(requests, list) or not 1 <= len(requests) <= 64:
        raise ValueError('batch size must be 1..64')
    # Return only a complete list. A bad row raises before any output is returned.
    return [predict(bundle, request, version) for request in requests]
Read the complete recorded gate output
Recorded local gate exerciseRecorded output · textDownload
{
  "execution_mode": "local exercise using a previously trained candidate",
  "candidate_sha256": "a4692350c5e6d1f231eea8c5eb3354c24276870e73a60922a10abb07abffa5f6",
  "candidate": {
    "candidate_sha256": "a4692350c5e6d1f231eea8c5eb3354c24276870e73a60922a10abb07abffa5f6",
    "partition": "validation",
    "policy": {
      "accuracy": [
        90,
        100
      ],
      "class_recall": [
        85,
        100
      ]
    },
    "membership_sha256": "7dd5f31b4d981bb1400c5dee0b845d14929a5936eb4516b6ad6c881870e4d544",
    "rows": 590,
    "correct": 547,
    "confusion": {
      "Cammeo": {
        "Cammeo": 223,
        "Osmancik": 25
      },
      "Osmancik": {
        "Cammeo": 18,
        "Osmancik": 324
      }
    },
    "passed": true
  },
  "constant_class_candidate": {
    "candidate_sha256": "e0fd221b389939213c0a1a15cb8d7b56220ca765c422981c3fff2b6ffdbf4868",
    "partition": "validation",
    "policy": {
      "accuracy": [
        90,
        100
      ],
      "class_recall": [
        85,
        100
      ]
    },
    "membership_sha256": "7dd5f31b4d981bb1400c5dee0b845d14929a5936eb4516b6ad6c881870e4d544",
    "rows": 590,
    "correct": 248,
    "confusion": {
      "Cammeo": {
        "Cammeo": 248,
        "Osmancik": 0
      },
      "Osmancik": {
        "Cammeo": 342,
        "Osmancik": 0
      }
    },
    "passed": false
  },
  "registration_refusal": "candidate failed quality policy",
  "files_after_failed_gate": 0
}

The exercise driver shows each operation and cleanup. The fresh-training driver and training run record document how the saved candidate was created. Adding --record to an exercise deliberately replaces its result and generated report; it does not refresh the screenshot automatically.

Failure cases

Trigger Required boundary and test
Failed class recall test_bad_model_cannot_register requires the constant-class model to fail without creating a registry file.
Candidate digest mismatch test_gate_uses_exact_counts_and_binds_candidate changes the candidate bytes while retaining the old expectation and requires refusal.

The recorded registration refusal is candidate failed quality policy. Valid dimensions and a newly computed digest cannot repair the failed task-quality check.

This is a fixed validation gate. Repeatedly selecting candidates or changing thresholds after reading its results can overfit the validation partition. The workflow performs no held-out test fitting, tuning or scoring. Passing does not establish calibration, independent test performance or a safe production release.

Deployment

Use the gate result to decide whether this candidate may enter the local registry. Keep its candidate identity, policy and validation membership together. A passing gate does not start a model server or authorize a traffic rollout; the registry lesson handles selection next.

Scaling

The current gate predicts every validation row synchronously. A larger evaluator may distribute predictions, but it must preserve row identity, complete coverage and failure accounting. Averages over only completed predictions can conceal failed cases. No distributed evaluator is implemented here.

Key takeaways

The candidate digest matches and its overall accuracy passes, but one class recall fails. What should registration do?

The candidate digest matches and its overall accuracy passes, but one class recall fails. What should registration do?

Choose one answer, then check your reasoning.

Read the explanation

Every required quality condition must pass for registration.

Best practices

Declare the policy before selecting candidates. Compare exact counts against thresholds. Keep validation selection separate from a final held-out assessment, and retain the failed candidate’s result for diagnosis.

References

Written by Arjun Prabhulal.

Module 4

Validate, register and serve a candidate