cropintel / ml /scripts /diagnose_pipeline.py
Jaithra Polavarapu
CropIntel — HF Space deploy (all-in-one app)
889dd1b
Raw
History Blame Contribute Delete
6.23 kB
#!/usr/bin/env python3
"""Empirical diagnostics for the CropIntel training pipeline.
Verifies the assumptions our preprocessing + augmentation depend on, instead of
trusting prior notes. Prints hard facts:
1. Does EfficientNetB0 (TF 2.21) contain a built-in Rescaling/Normalization?
2. What value range does ImageDataGenerator emit after augmentation
(brightness_range is the classic [0,1] -> [0,255] offender)?
3. Real loaded-data value range.
4. Can a frozen-backbone head overfit a tiny batch (sanity of gradients)?
"""
import os
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
import numpy as np
import tensorflow as tf
from tensorflow.keras import applications, layers
def section(title):
print("\n" + "=" * 70)
print(title)
print("=" * 70)
# ---------------------------------------------------------------------------
section("1. EfficientNetB0 internal preprocessing layers (TF %s)" % tf.__version__)
# ---------------------------------------------------------------------------
eff = applications.EfficientNetB0(include_top=False, weights=None,
input_shape=(224, 224, 3))
preproc_layers = []
for lyr in eff.layers[:6]:
kind = type(lyr).__name__
info = ""
if kind == "Rescaling":
info = f"scale={lyr.scale} offset={lyr.offset}"
elif kind == "Normalization":
info = f"mean={getattr(lyr, 'mean', None)} var={getattr(lyr, 'variance', None)}"
print(f" layer[{lyr.name}] = {kind} {info}")
if kind in ("Rescaling", "Normalization"):
preproc_layers.append((lyr.name, kind, info))
print(f"\n -> built-in preprocessing layers found: {preproc_layers or 'NONE'}")
# Probe behaviour: feed known constant images, observe output stats.
for val, desc in [(1.0, "[0,1] max (1.0)"), (255.0, "[0,255] max"), (0.5, "mid 0.5")]:
probe = np.full((1, 224, 224, 3), val, dtype=np.float32)
out = eff(probe, training=False).numpy()
print(f" input const={val:6.1f} ({desc:16s}) -> backbone out "
f"min={out.min():.4f} max={out.max():.4f} mean={out.mean():.4f}")
# ---------------------------------------------------------------------------
section("2. ImageDataGenerator output range (augmentation value-range check)")
# ---------------------------------------------------------------------------
# Synthetic [0,1] batch
x = np.random.rand(8, 224, 224, 3).astype(np.float32)
y = tf.keras.utils.to_categorical(np.array([0, 1, 2, 3, 0, 1, 2, 3]), 4)
print(f" source batch range: min={x.min():.4f} max={x.max():.4f}")
for label, kwargs in [
("no-aug", {}),
("aug WITHOUT brightness", dict(rotation_range=30, horizontal_flip=True,
zoom_range=0.3, fill_mode="nearest")),
("aug WITH brightness_range", dict(rotation_range=30, horizontal_flip=True,
zoom_range=0.3, brightness_range=[0.8, 1.2],
fill_mode="nearest")),
]:
gen = tf.keras.preprocessing.image.ImageDataGenerator(**kwargs)
flow = gen.flow(x, y, batch_size=8, shuffle=False)
bx, _ = next(flow)
print(f" {label:28s} -> min={bx.min():8.4f} max={bx.max():8.4f} "
f"mean={bx.mean():.4f}")
# ---------------------------------------------------------------------------
section("3. Real data value range (first available crop)")
# ---------------------------------------------------------------------------
from ml.config import CROPS # noqa: E402
from ml.utils.data_loader import CropDatasetLoader # noqa: E402
for crop in CROPS:
try:
loader = CropDatasetLoader(crop)
imgs, labels, names = loader.load_dataset()
print(f" {crop}: {len(imgs)} imgs range=[{imgs.min():.4f},{imgs.max():.4f}] "
f"classes={names}")
# label-image coupling spot check: show class of 3 random samples and
# confirm the label index maps to a sane class name
rng = np.random.default_rng(0)
for i in rng.choice(len(imgs), size=3, replace=False):
print(f" sample[{i}] label={labels[i]} -> {names[labels[i]]} "
f"img_mean={imgs[i].mean():.3f}")
break
except Exception as e:
print(f" {crop}: load failed ({e})")
continue
# ---------------------------------------------------------------------------
section("4. Can the head overfit a tiny batch? (gradient sanity)")
# ---------------------------------------------------------------------------
# If a frozen-backbone + head CANNOT drive train accuracy to ~100% on 32 images
# in 30 steps, the features reaching the head are broken (preprocessing) — NOT a
# data or hyperparameter problem.
def build_probe(rescale_scale, rescale_offset):
inp = tf.keras.Input((224, 224, 3))
z = layers.Rescaling(rescale_scale, rescale_offset)(inp)
base = applications.EfficientNetB0(include_top=False, weights="imagenet",
input_shape=(224, 224, 3))
base.trainable = False
z = base(z, training=False)
z = layers.GlobalAveragePooling2D()(z)
z = layers.Dense(64, activation="relu")(z)
out = layers.Dense(4, activation="softmax")(z)
m = tf.keras.Model(inp, out)
m.compile(optimizer=tf.keras.optimizers.Adam(1e-3),
loss="categorical_crossentropy", metrics=["accuracy"])
return m
try:
loader = CropDatasetLoader(next(iter(CROPS)))
imgs, labels, names = loader.load_dataset()
idx = np.arange(len(imgs))[:32]
bx = imgs[idx]
by = tf.keras.utils.to_categorical(labels[idx], len(names))
for scale, offset, desc in [(255.0, 0.0, "Rescaling(255,0) [current]"),
(1.0, 0.0, "Rescaling(1,0)=identity [0,1]"),
(2.0, -1.0, "Rescaling(2,-1)=[-1,1]")]:
m = build_probe(scale, offset)
h = m.fit(bx, by, epochs=30, batch_size=32, verbose=0)
print(f" {desc:34s} -> final train_acc={h.history['accuracy'][-1]:.3f} "
f"loss={h.history['loss'][-1]:.4f}")
except Exception as e:
print(f" overfit probe failed: {e}")
print("\nDIAGNOSTIC COMPLETE")